//
//  TCPBaseManager.swift
//  TimeBoat
//
//  Created by Jun on 2022/9/8.
//  Copyright © 2022 Time Boat. All rights reserved.
//

import Foundation
import CocoaAsyncSocket

@objc protocol TCPBaseManagerDelegate: NSObjectProtocol {
    @objc optional func didReceiveData(data: Data);
}

@objcMembers class TCPBaseManager: NSObject {
    @objc static let shared = TCPBaseManager()
    
    private var mSocket: GCDAsyncSocket?
    private let mDispatchQueue = DispatchQueue.init(label: "tcp-connect-queue")
    private let TAG = LogTag.init(rawValue: "TCPBaseManager")
    public var delegate: TCPBaseManagerDelegate?
    private var mSocketTag: Int = 0
    private var socketTag: Int {
        get {
            mSocketTag += 1
            return mSocketTag
        }
    }
    
    public func start() {
        Log.debug(tag: TAG, "start")
        createSocket()
        connect()
    }
    
    private func createSocket() {
        if nil == mSocket {
            mSocket = GCDAsyncSocket(delegate: self, delegateQueue: mDispatchQueue)
        }
    }
    
    public func disconnect() {
        mSocket?.disconnect()
    }
    
    private func getHost() -> String {
        return ConfigManager.shared.getTCPHost()
    }
    
    private func getPort() -> UInt16 {
        return ConfigManager.shared.getTCPPort()
    }
    
    private func connect() {
        do {
            createSocket()
            try mSocket?.connect(toHost: getHost(), onPort: getPort())
        } catch _ {
            mSocket = nil
        }
    }
    
    public func isConnected() -> Bool {
        var connected = false
        
        if nil != mSocket {
            connected = mSocket!.isConnected
        }
        
        return connected
    }
    
    public func writeCommandData(data: Data?) {
        if !isConnected() {
            createSocket()
            connect()
        }
        
        write(data: data)
    }
    
    private func write(data: Data?) {
        if let buf = data {
            if let str = String.init(data: buf, encoding: .utf8) {
                Log.debug(tag: TAG, "write cmd = " + str)
            }
            
            mSocket?.write(buf, withTimeout: 3, tag: socketTag)
        }
    }
}

extension TCPBaseManager: GCDAsyncSocketDelegate {
    /**
     * This method is called immediately prior to socket:didAcceptNewSocket:.
     * It optionally allows a listening socket to specify the socketQueue for a new accepted socket.
     * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue.
     *
     * Since you cannot autorelease a dispatch_queue,
     * this method uses the "new" prefix in its name to specify that the returned queue has been retained.
     *
     * Thus you could do something like this in the implementation:
     * return dispatch_queue_create("MyQueue", NULL);
     *
     * If you are placing multiple sockets on the same queue,
     * then care should be taken to increment the retain count each time this method is invoked.
     *
     * For example, your implementation might look something like this:
     * dispatch_retain(myExistingQueue);
     * return myExistingQueue;
    **/
    func newSocketQueueForConnection(fromAddress address: Data, on sock: GCDAsyncSocket) -> DispatchQueue? {
        Log.debug(tag: TAG, "new socket queue for connection")
        
        return mDispatchQueue
    }
    
    /**
     * Called when a socket accepts a connection.
     * Another socket is automatically spawned to handle it.
     *
     * You must retain the newSocket if you wish to handle the connection.
     * Otherwise the newSocket instance will be released and the spawned connection will be closed.
     *
     * By default the new socket will have the same delegate and delegateQueue.
     * You may, of course, change this at any time.
    **/
    func socket(_ sock: GCDAsyncSocket, didAcceptNewSocket newSocket: GCDAsyncSocket) {
        Log.debug(tag: TAG, "did accept new socket")
    }
    
    /**
     * Called when a socket connects and is ready for reading and writing.
     * The host parameter will be an IP address, not a DNS name.
    **/
    func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) {
        Log.debug(tag: TAG, "did connect to \(host), \(port)")
    }
    
    /**
     * Called when a socket connects and is ready for reading and writing.
     * The host parameter will be an IP address, not a DNS name.
     **/
    func socket(_ sock: GCDAsyncSocket, didConnectTo url: URL) {
        Log.debug(tag: TAG, "did connec to url \(url)")
    }
    
    /**
     * Called when a socket has completed reading the requested data into memory.
     * Not called if there is an error.
    **/
    func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) {
        Log.debug(tag: TAG, "did read data = \(data), tag = \(tag)")
        
        delegate?.didReceiveData?(data: data)
    }
    
    /**
     * Called when a socket has read in data, but has not yet completed the read.
     * This would occur if using readToData: or readToLength: methods.
     * It may be used for things such as updating progress bars.
    **/
    func socket(_ sock: GCDAsyncSocket, didReadPartialDataOfLength partialLength: UInt, tag: Int) {
        Log.debug(tag: TAG, "did read partial data of lenth = \(partialLength), tag = \(tag)")
    }
    
    /**
     * Called when a socket has completed writing the requested data. Not called if there is an error.
    **/
    func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) {
        Log.debug(tag: TAG, "did write data tag = \(tag)")
    }
    
    /**
     * Called when a socket has written some data, but has not yet completed the entire write.
     * It may be used for things such as updating progress bars.
    **/
    func socket(_ sock: GCDAsyncSocket, didWritePartialDataOfLength partialLength: UInt, tag: Int) {
        Log.debug(tag: TAG, "did write partial data of length = \(partialLength), tag = \(tag)")
    }
    
    /**
     * Called if a read operation has reached its timeout without completing.
     * This method allows you to optionally extend the timeout.
     * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount.
     * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual.
     *
     * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method.
     * The length parameter is the number of bytes that have been read so far for the read operation.
     *
     * Note that this method may be called multiple times for a single read if you return positive numbers.
    **/
    func socket(_ sock: GCDAsyncSocket, shouldTimeoutReadWithTag tag: Int, elapsed: TimeInterval, bytesDone length: UInt) -> TimeInterval {
        Log.debug(tag: TAG, "should timeout reading tag = \(tag), elapsed = \(elapsed), length = \(length)")
        return 10
    }
    
    /**
     * Called if a write operation has reached its timeout without completing.
     * This method allows you to optionally extend the timeout.
     * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount.
     * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual.
     *
     * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method.
     * The length parameter is the number of bytes that have been written so far for the write operation.
     *
     * Note that this method may be called multiple times for a single write if you return positive numbers.
    **/
    func socket(_ sock: GCDAsyncSocket, shouldTimeoutWriteWithTag tag: Int, elapsed: TimeInterval, bytesDone length: UInt) -> TimeInterval {
        Log.debug(tag: TAG, "should timeout write tag = \(tag), elapsed = \(elapsed), length = \(length)")
        return 10
    }
    
    /**
     * Conditionally called if the read stream closes, but the write stream may still be writeable.
     *
     * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO.
     * See the discussion on the autoDisconnectOnClosedReadStream method for more information.
    **/
    func socketDidCloseReadStream(_ sock: GCDAsyncSocket) {
        Log.debug(tag: TAG, "did close read stream")
    }
    
    /**
     * Called when a socket disconnects with or without error.
     *
     * If you call the disconnect method, and the socket wasn't already disconnected,
     * then an invocation of this delegate method will be enqueued on the delegateQueue
     * before the disconnect method returns.
     *
     * Note: If the GCDAsyncSocket instance is deallocated while it is still connected,
     * and the delegate is not also deallocated, then this method will be invoked,
     * but the sock parameter will be nil. (It must necessarily be nil since it is no longer available.)
     * This is a generally rare, but is possible if one writes code like this:
     *
     * asyncSocket = nil; // I'm implicitly disconnecting the socket
     *
     * In this case it may preferrable to nil the delegate beforehand, like this:
     *
     * asyncSocket.delegate = nil; // Don't invoke my delegate method
     * asyncSocket = nil; // I'm implicitly disconnecting the socket
     *
     * Of course, this depends on how your state machine is configured.
    **/
    func socketDidDisconnect(_ sock: GCDAsyncSocket, withError err: Error?) {
        Log.debug(tag: TAG, "did disconnect")
    }
    
    /**
     * Called after the socket has successfully completed SSL/TLS negotiation.
     * This method is not called unless you use the provided startTLS method.
     *
     * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close,
     * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code.
    **/
    func socketDidSecure(_ sock: GCDAsyncSocket) {
        Log.debug(tag: TAG, "socket did secure")
    }
}

